package com.yanboo.processor;

import org.apache.commons.exec.*;
import org.apache.commons.exec.launcher.CommandLauncher;
import org.apache.commons.exec.launcher.CommandLauncherFactory;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.function.Supplier;

public class CustomDefaultExecutor {

    /**
     * Constructs a new builder.
     *
     * @param <T> The builder type.
     * @since 1.4.0
     */
    public static class Builder<T extends CustomDefaultExecutor.Builder<T>> implements Supplier<CustomDefaultExecutor> {

        private ThreadFactory threadFactory;
        private ExecuteStreamHandler executeStreamHandler;
        private File workingDirectory;

        @SuppressWarnings("unchecked")
        T asThis() {
            return (T) this;
        }

        /**
         * Creates a new configured DefaultExecutor.
         *
         * @return a new configured DefaultExecutor.
         */
        @Override
        public CustomDefaultExecutor get() {
            return new CustomDefaultExecutor(threadFactory, executeStreamHandler, workingDirectory);
        }

        ExecuteStreamHandler getExecuteStreamHandler() {
            return executeStreamHandler;
        }

        ThreadFactory getThreadFactory() {
            return threadFactory;
        }

        File getWorkingDirectory() {
            return workingDirectory;
        }

        /**
         * Sets the PumpStreamHandler.
         *
         * @param executeStreamHandler the ExecuteStreamHandler, null resets to the default.
         * @return this.
         */
        public T setExecuteStreamHandler(final ExecuteStreamHandler executeStreamHandler) {
            this.executeStreamHandler = executeStreamHandler;
            return asThis();
        }

        /**
         * Sets the ThreadFactory.
         *
         * @param threadFactory the ThreadFactory, null resets to the default.
         * @return this.
         */
        public T setThreadFactory(final ThreadFactory threadFactory) {
            this.threadFactory = threadFactory;
            return asThis();
        }

        /**
         * Sets the working directory.
         *
         * @param workingDirectory the working directory., null resets to the default.
         * @return this.
         */
        public T setWorkingDirectory(final File workingDirectory) {
            this.workingDirectory = workingDirectory;
            return asThis();
        }

    }

    /**
     * Creates a new builder.
     *
     * @return a new builder.
     * @since 1.4.0
     */
    public static CustomDefaultExecutor.Builder<?> builder() {
        return new CustomDefaultExecutor.Builder<>();
    }

    /**
     * Taking care of output and error stream.
     */
    private ExecuteStreamHandler executeStreamHandler;

    /**
     * The working directory of the process.
     */
    private File workingDirectory;

    /**
     * Monitoring of long running processes.
     */
    private ExecuteWatchdog watchdog;

    /**
     * The exit values considered to be successful.
     */
    private int[] exitValues;

    /**
     * Launches the command in a new process.
     */
    private final CommandLauncher launcher;

    /**
     * Optional cleanup of started processes.
     */
    private ProcessDestroyer processDestroyer;

    /**
     * Worker thread for asynchronous execution.
     */
    private Thread executorThread;

    /**
     * The first exception being caught to be thrown to the caller.
     */
    private IOException exceptionCaught;

    /**
     * The thread factory.
     */
    private final ThreadFactory threadFactory;

    /**
     * Constructs a default {@code PumpStreamHandler} and sets the working directory of the subprocess to the current working directory.
     * <p>
     * The {@code PumpStreamHandler} pumps the output of the subprocess into our {@code System.out} and {@code System.err} to avoid into our {@code System.out}
     * and {@code System.err} to avoid a blocked or deadlocked subprocess (see {@link Process Process}).
     *
     * @deprecated Use {@link org.apache.commons.exec.DefaultExecutor.Builder#get()}.
     */
    @Deprecated
    public CustomDefaultExecutor() {
        this(Executors.defaultThreadFactory(), new PumpStreamHandler(), new File("."));
    }

    CustomDefaultExecutor(final ThreadFactory threadFactory, final ExecuteStreamHandler executeStreamHandler, final File workingDirectory) {
        this.threadFactory = threadFactory != null ? threadFactory : Executors.defaultThreadFactory();
        this.executeStreamHandler = executeStreamHandler != null ? executeStreamHandler : new PumpStreamHandler();
        this.workingDirectory = workingDirectory != null ? workingDirectory : new File(".");
        this.launcher = CommandLauncherFactory.createVMLauncher();
        this.exitValues = new int[0];
    }

    private void checkWorkingDirectory() throws IOException {
        checkWorkingDirectory(workingDirectory);
    }

    private void checkWorkingDirectory(final File directory) throws IOException {
        if (directory != null && !directory.exists()) {
            throw new IOException(directory + " doesn't exist.");
        }
    }

    /**
     * Closes the Closeable, remembering any exception.
     *
     * @param closeable the {@link Closeable} to close.
     */
    private void closeCatch(final Closeable closeable) {
        try {
            closeable.close();
        } catch (final IOException e) {
            setExceptionCaught(e);
        }
    }

    /**
     * Closes the streams belonging to the given Process.
     *
     * @param process the {@link Process}.
     */
    @SuppressWarnings("resource")
    private void closeProcessStreams(final Process process) {
        closeCatch(process.getInputStream());
        closeCatch(process.getOutputStream());
        closeCatch(process.getErrorStream());
    }

    /**
     * Creates a thread waiting for the result of an asynchronous execution.
     *
     * @param runnable the runnable passed to the thread.
     * @param name     the name of the thread.
     * @return the thread
     */
    protected Thread createThread(final Runnable runnable, final String name) {
        return ThreadUtil.newThread(threadFactory, runnable, name, false);
    }

    /**
     * @see org.apache.commons.exec.Executor#execute(CommandLine)
     */
    public void execute(final CommandLine command,ProcessCallBack callBack) throws ExecuteException, IOException {
         execute(command, (Map<String, String>) null, callBack);
    }

    /**
     * @see org.apache.commons.exec.Executor#execute(CommandLine, org.apache.commons.exec.ExecuteResultHandler)
     */
    public void execute(final CommandLine command, final ExecuteResultHandler handler,ProcessCallBack callBack) throws ExecuteException, IOException {
        execute(command, null, handler, callBack);
    }

    /**
     * @see org.apache.commons.exec.Executor#execute(CommandLine, java.util.Map)
     */
    public void execute(final CommandLine command, final Map<String, String> environment,ProcessCallBack callBack) throws ExecuteException, IOException {
        checkWorkingDirectory();
        executeInternal(command, environment, workingDirectory, executeStreamHandler, callBack);
    }

    /**
     * @see org.apache.commons.exec.Executor#execute(CommandLine, java.util.Map, org.apache.commons.exec.ExecuteResultHandler)
     */
    public void execute(final CommandLine command, final Map<String, String> environment, final ExecuteResultHandler handler,ProcessCallBack callBack)
            throws ExecuteException, IOException {
        checkWorkingDirectory();
        if (watchdog != null) {
            Class<? extends ExecuteWatchdog> watchdogClass = watchdog.getClass();
            Method setProcessNotStarted = null;
            try {
                setProcessNotStarted = watchdogClass.getDeclaredMethod("setProcessNotStarted");
            } catch (NoSuchMethodException e) {
                throw new RuntimeException(e);
            }
            setProcessNotStarted.setAccessible(true);
            try {
                setProcessNotStarted.invoke(watchdog);
            } catch (IllegalAccessException | InvocationTargetException e) {
                throw new RuntimeException(e);
            }
            // watchdog.setProcessNotStarted();
        }

        executorThread = createThread(() -> {
            int exitValue = Executor.INVALID_EXITVALUE;
            try {
                Process process = executeInternal(command, environment, workingDirectory, executeStreamHandler, callBack);
                exitValue = process.exitValue();
                handler.onProcessComplete(exitValue);
            } catch (final ExecuteException e) {
                handler.onProcessFailed(e);
            } catch (final Exception e) {
                handler.onProcessFailed(new ExecuteException("Execution failed", exitValue, e));
            }
        }, "CommonsExecDefaultExecutor");
        getExecutorThread().start();
    }

    /**
     * Execute an internal process. If the executing thread is interrupted while waiting for the child process to return the child process will be killed.
     *
     * @param command          the command to execute.
     * @param environment      the execution environment.
     * @param workingDirectory the working directory.
     * @param streams          process the streams (in, out, err) of the process.
     * @return the exit code of the process.
     * @throws IOException executing the process failed.
     */
    private Process executeInternal(final CommandLine command, final Map<String, String> environment, final File workingDirectory,
                                    final ExecuteStreamHandler streams,ProcessCallBack callBack ) throws IOException {
        final Process process;
        exceptionCaught = null;
        try {
            process = launch(command, environment, workingDirectory);

        } catch (final IOException e) {
            if (watchdog != null) {
                watchdog.failedToStart(e);
            }
            throw e;
        }
        try {
            setStreams(streams, process);
        } catch (final IOException e) {
            process.destroy();
            if (watchdog != null) {
                watchdog.failedToStart(e);
            }
            throw e;
        }
        streams.start();
        try {
            // add the process to the list of those to destroy if the VM exits
            if (getProcessDestroyer() != null) {
                getProcessDestroyer().add(process);
            }
            // associate the watchdog with the newly created process
            if (watchdog != null) {
                watchdog.start(process);
            }
            int exitValue = Executor.INVALID_EXITVALUE;
            try {
                callBack.onProcessRunning(process);
                exitValue = process.waitFor();
            } catch (final InterruptedException e) {
                process.destroy();
            } finally {
                // see http://bugs.sun.com/view_bug.do?bug_id=6420270
                // see https://issues.apache.org/jira/browse/EXEC-46
                // Process.waitFor should clear interrupt status when throwing InterruptedException
                // but we have to do that manually
                Thread.interrupted();
            }
            if (watchdog != null) {
                watchdog.stop();
            }
            try {
                streams.stop();
            } catch (final IOException e) {
                setExceptionCaught(e);
            }
            closeProcessStreams(process);
            if (getExceptionCaught() != null) {
                throw getExceptionCaught();
            }
            if (watchdog != null) {
                try {
                    watchdog.checkException();
                } catch (final IOException e) {
                    throw e;
                } catch (final Exception e) {
                    throw new IOException(e);
                }
            }
            if (isFailure(exitValue)) {
                throw new ExecuteException("Process exited with an error: " + exitValue, exitValue);
            }
            return process;
        } finally {
            // remove the process to the list of those to destroy if the VM exits
            if (getProcessDestroyer() != null) {
                getProcessDestroyer().remove(process);
            }
        }
    }

    /**
     * Gets the first IOException being thrown.
     *
     * @return the first IOException being caught.
     */
    private IOException getExceptionCaught() {
        return exceptionCaught;
    }

    /**
     * Gets the worker thread being used for asynchronous execution.
     *
     * @return the worker thread.
     */
    protected Thread getExecutorThread() {
        return executorThread;
    }

    /**
     * @see org.apache.commons.exec.Executor#getProcessDestroyer()
     */
    public ProcessDestroyer getProcessDestroyer() {
        return processDestroyer;
    }

    /**
     * @see org.apache.commons.exec.Executor#getStreamHandler()
     */
    public ExecuteStreamHandler getStreamHandler() {
        return executeStreamHandler;
    }

    /**
     * Gets the thread factory. Z
     *
     * @return the thread factory.
     */
    ThreadFactory getThreadFactory() {
        return threadFactory;
    }

    /**
     * @see org.apache.commons.exec.Executor#getWatchdog()
     */
    public ExecuteWatchdog getWatchdog() {
        return watchdog;
    }

    /**
     * @see org.apache.commons.exec.Executor#getWorkingDirectory()
     */
    public File getWorkingDirectory() {
        return workingDirectory;
    }

    /**
     * @see org.apache.commons.exec.Executor#isFailure(int)
     */
    public boolean isFailure(final int exitValue) {
        if (exitValues == null) {
            return false;
        }
        if (exitValues.length == 0) {
            return launcher.isFailure(exitValue);
        }
        for (final int exitValue2 : exitValues) {
            if (exitValue2 == exitValue) {
                return false;
            }
        }
        return true;
    }

    /**
     * Creates a process that runs a command.
     *
     * @param command          the command to run.
     * @param env              the environment for the command.
     * @param workingDirectory the working directory for the command.
     * @return the process started.
     * @throws IOException forwarded from the particular launcher used.
     */
    protected Process launch(final CommandLine command, final Map<String, String> env, final File workingDirectory) throws IOException {
        if (launcher == null) {
            throw new IllegalStateException("CommandLauncher can not be null");
        }
      // ControlDaemon controlDaemon = new ControlDaemon(command, env, workingDirectory);
        checkWorkingDirectory(workingDirectory);
        return launcher.exec(command, env, workingDirectory);
    }

    /**
     * Sets the first IOException thrown.
     *
     * @param e the IOException.
     */
    private void setExceptionCaught(final IOException e) {
        if (exceptionCaught == null) {
            exceptionCaught = e;
        }
    }

    /**
     * @see org.apache.commons.exec.Executor#setExitValue(int)
     */
    public void setExitValue(final int value) {
        setExitValues(new int[]{value});
    }

    /**
     * @see org.apache.commons.exec.Executor#setExitValues(int[])
     */
    public void setExitValues(final int[] values) {
        exitValues = values == null ? null : (int[]) values.clone();
    }

    /**
     * @see org.apache.commons.exec.Executor#setProcessDestroyer(ProcessDestroyer)
     */
    public void setProcessDestroyer(final ProcessDestroyer processDestroyer) {
        this.processDestroyer = processDestroyer;
    }

    /**
     * @see org.apache.commons.exec.Executor#setStreamHandler(org.apache.commons.exec.ExecuteStreamHandler)
     */
    public void setStreamHandler(final ExecuteStreamHandler streamHandler) {
        this.executeStreamHandler = streamHandler;
    }

    @SuppressWarnings("resource")
    private void setStreams(final ExecuteStreamHandler streams, final Process process) throws IOException {

        streams.setProcessInputStream(process.getOutputStream());
        streams.setProcessOutputStream(process.getInputStream());
        streams.setProcessErrorStream(process.getErrorStream());
    }

    /**
     * @see org.apache.commons.exec.Executor#setWatchdog(org.apache.commons.exec.ExecuteWatchdog)
     */
    public void setWatchdog(final ExecuteWatchdog watchdog) {
        this.watchdog = watchdog;
    }

    /**
     * Sets the working directory.
     *
     * @see org.apache.commons.exec.Executor#setWorkingDirectory(java.io.File)
     * @deprecated Use {@link org.apache.commons.exec.DefaultExecutor.Builder#setWorkingDirectory(File)}.
     */
    @Deprecated
    public void setWorkingDirectory(final File workingDirectory) {
        this.workingDirectory = workingDirectory;
    }

}
